Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

POC Memory Tracking of Potentially Shared Buffer #6590

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from

Conversation

tustvold
Copy link
Contributor

@tustvold tustvold commented Oct 19, 2024

Which issue does this PR close?

Relates to #6439.

Rationale for this change

A POC showing how we could add more accurate memory tracking to arrays without a huge amount of churn

What changes are included in this PR?

This adds two traits MemoryPool and MemoryReservation that work together to allow accurate instrumentation of Buffer memory usage.

Are there any user-facing changes?

@github-actions github-actions bot added the arrow Changes to the arrow crate label Oct 19, 2024
Copy link
Contributor Author

@tustvold tustvold left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should provide a mechanism for consumers like DF to track memory usage of buffered arrays accurately, and with performance that is no worse than the current get_buffer_memory_size, which this would likely lead us to deprecate.

@@ -44,6 +44,9 @@ pub struct Bytes {

/// how to deallocate this region
deallocation: Deallocation,

#[cfg(feature = "pool")]
reservation: std::sync::Mutex<Option<Box<dyn crate::MemoryReservation>>>,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I originally used parking_lot::Mutex but this would have made Buffer not RewindSafe, which would be a breaking change (and given there is a test for this I presume this is important).


/// A memory reservation within a [`MemoryPool`] that is freed on drop
pub trait MemoryReservation {
fn resize(&mut self, new: usize);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't actually have a use-case for this atm, but this seemed sensible to add now to avoid it being difficult later

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mutable buffer might need this. It may resize its reservation dynamically

/// Register this [`Bytes`] with the provided [`MemoryPool`]
#[cfg(feature = "pool")]
pub fn claim(&self, pool: &dyn crate::MemoryPool) {
*self.reservation.lock().unwrap() = Some(pool.register(self.capacity()));
Copy link
Contributor Author

@tustvold tustvold Oct 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are two implications of this formulation:

  • We always allocate a new memory reservation which could be wasteful
  • We allocate from the new pool before freeing the memory from the previous reservation


/// Register this [`Buffer`] with the provided [`MemoryPool`]
#[cfg(feature = "pool")]
pub fn claim(&self, pool: &dyn crate::MemoryPool) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could plumb this claim API through the various arrays and RecordBatch. Consumers like DF could then claim RecordBatch they wish to buffer, and know they aren't double-counting.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is interesting -- so the idea is that this would support cooperatively assigning / tracking usage (rather than relying on some global allocator to do so).

That aligns pretty nicely with the "no overhead unless asked" and "everything being explicit" principles

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this API would be easier to use / manage if it also returned the prior reservation, if any

Copy link
Contributor Author

@tustvold tustvold Oct 21, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this would make it harder to chain through to Arrays and RecordBatch that may consist of multiple buffers and therefore reservations. It would also mean potentially keeping around "duplicate" reservations for longer than necessary, which would become even more problematic if the same buffer is used multiple times.

What would be the use-case for the returned reservation?

Copy link
Contributor

@alamb alamb Oct 21, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking like if only some Arrays in a RecordBatch have a reservation but some didn't and then a system like the DataFusion memory pool added an entirely new reservation for all the arrays wiping out the old reservations

In that case I would want the previous reservations to be unregistered

Although maybe Drop that did the unregistering would be good enough for that case 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although maybe Drop that did the unregistering would be good enough for that case 🤔

That's what is intended, and follows RAII best-practices

[lib]
name = "arrow_buffer"
path = "src/lib.rs"
bench = false

[features]
pool = []
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As it is very hard to guage what if any performance implications there are of this, I think it is important that it is gated by a feature flag at least initially.

@tustvold tustvold changed the title POC MemoryPool POC Memory Tracking of Potentially Shared Buffer Oct 19, 2024
Copy link
Contributor

@alamb alamb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you @tustvold -- this looks very cool

My suggested next steps are twofold:

  1. Write up some self contained example showing how to use this API in a real world scenario (perhaps @haohuaijin 's The get_array_memory_size() get wrong result(with different compression method) after deconde record from ipc format #6363 of tracking memory sent to/from IPC buffers)
  2. Try and plumb it into DataFusion's MemoryPool and see how that looks / if this API is sufficient

cc @waynexia and @jhorstmann in case you are interested


/// Register this [`Buffer`] with the provided [`MemoryPool`]
#[cfg(feature = "pool")]
pub fn claim(&self, pool: &dyn crate::MemoryPool) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is interesting -- so the idea is that this would support cooperatively assigning / tracking usage (rather than relying on some global allocator to do so).

That aligns pretty nicely with the "no overhead unless asked" and "everything being explicit" principles


/// Register this [`Buffer`] with the provided [`MemoryPool`]
#[cfg(feature = "pool")]
pub fn claim(&self, pool: &dyn crate::MemoryPool) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this API would be easier to use / manage if it also returned the prior reservation, if any

@waynexia waynexia self-requested a review October 21, 2024 14:20
@waynexia
Copy link
Member

Thanks for bringing this up 👍 I'll take a closer look tomorrow

@waynexia
Copy link
Member

This looks fits the requirement of precise memory tracking without touching the unstable rust feature 💯

Adding an explicit API looks fine to me. I'd like to give it a try on mutable buffer either to see if there is any hiding problem

@waynexia
Copy link
Member

Implementation for MutableBuffer is tustvold#82

On writing that patch I find resize is indeed useful. And in one case I need .resize(0) to reflect one memory is deallocated.

This API LGTM in general.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
arrow Changes to the arrow crate
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants